# frozen_string_literal: true

require 'parser/ruby27'

module Gitlab
  module BackgroundMigration
    # This migration fixes raw_metadata entries which have incorrectly been passed a Ruby Hash instead of JSON data.
    class FixVulnerabilityOccurrencesWithHashesAsRawMetadata
      CLUSTER_IMAGE_SCANNING_REPORT_TYPE = 7
      GENERIC_REPORT_TYPE = 99

      # Type error is used to handle unexpected types when parsing stringified hashes.
      class TypeError < ::StandardError
        attr_reader :message, :type

        def initialize(message, type)
          @message = message
          @type = type
        end
      end

      # Migration model namespace isolated from application code.
      class Finding < ActiveRecord::Base
        include EachBatch

        self.table_name = 'vulnerability_occurrences'

        scope :by_api_report_types, -> { where(report_type: [CLUSTER_IMAGE_SCANNING_REPORT_TYPE, GENERIC_REPORT_TYPE]) }
      end

      def perform(start_id, end_id)
        Finding.by_api_report_types.where(id: start_id..end_id).each do |finding|
          next if valid_json?(finding.raw_metadata)

          metadata = hash_from_s(finding.raw_metadata)

          finding.update(raw_metadata: metadata.to_json) if metadata
        end
        mark_job_as_succeeded(start_id, end_id)
      end

      def hash_from_s(str_hash)
        ast = Parser::Ruby27.parse(str_hash)

        unless ast.type == :hash
          ::Gitlab::AppLogger.error(message: "expected raw_metadata to be a hash", type: ast.type)
          return
        end

        parse_hash(ast)
      rescue Parser::SyntaxError => e
        ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message)
        nil
      rescue TypeError => e
        ::Gitlab::AppLogger.error(message: "error parsing raw_metadata", error: e.message, type: e.type)
        nil
      end

      private

      def mark_job_as_succeeded(*arguments)
        ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
          'FixVulnerabilityOccurrencesWithHashesAsRawMetadata',
          arguments
        )
      end

      def valid_json?(metadata)
        Oj.load(metadata)
        true
      rescue Oj::ParseError, EncodingError, JSON::ParserError, JSON::GeneratorError, Encoding::UndefinedConversionError
        false
      end

      def parse_hash(hash)
        out = {}
        hash.children.each do |node|
          unless node.type == :pair
            raise TypeError.new("expected child of hash to be a `pair`", node.type)
          end

          key, value = node.children

          key = parse_key(key)
          value = parse_value(value)

          out[key] = value
        end

        out
      end

      def parse_key(key)
        case key.type
        when :sym, :str, :int
          key.children.first
        else
          raise TypeError.new("expected key to be either symbol, string, or integer", key.type)
        end
      end

      def parse_value(value)
        case value.type
        when :sym, :str, :int
          value.children.first
        # rubocop:disable Lint/BooleanSymbol
        when :true
          true
        when :false
          false
        # rubocop:enable Lint/BooleanSymbol
        when :nil
          nil
        when :array
          value.children.map { |c| parse_value(c) }
        when :hash
          parse_hash(value)
        else
          raise TypeError.new("value of a pair was an unexpected type", value.type)
        end
      end
    end
  end
end
